/*

 MSpaceInvaders
 
 Copyright (c) 2007 Mary Jane Soft - Marlon J. Manrique
 	
 http://mjs.darkgreenmedia.com
 http://marlonj.darkgreenmedia.com
 
 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation; either version 2
 of the License, or (at your option) any later version.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
 $Id: MSIGame.pde 273 2007-07-25 20:21:44Z marlonj $
 	
*/

/**
 * Implementation of the SpaceInaver game using the M3d library to provide
 * a point of view different to the player.
 */
public class MSIGame
{
  // The shelter structure
  int[][] shelter =
  {
    { 0,1,1,1,1,1,0 },
    { 1,1,1,1,1,1,1 },
    { 1,1,0,0,0,1,1 },
    { 1,1,0,0,0,1,1 }
  };

  // The active camera
  MCamera cam;

  // Images invaders, spaceships and player
  PImage si1_1;
  PImage si1_2;
  PImage player_1;
  PImage spaceship_1;

  // Dimensions of a space invader
  int siWidth;
  int siHeight;

  // Player direction and player object
  int playerDir = -1;
  MSprite3D player;

  // Num of alive invaders and invaders objects and directions
  int numInvaders;
  MSprite3D[][] invaders;
  boolean right = true;
  
  // Invaders movements and speed
  int steps = 10;
  int countSteps = 5;
  int speed = 10;  // Number of frames before move invaders

  // Player bullets, maximum, current number and objects
  int maxPlayerBullets;
  int numPlayerBullets;
  MMesh[] playerBullets;

  // Invaders bullets, maximum, current number and objectsv
  int maxInvadersBullets;
  int numInvadersBullets;
  MMesh[] invadersBullets;

  // Spaceship direction and object
  int spaceshipDir;  
  MSprite3D spaceship;
  
  // Blocks of all the shelters
  MMesh[] shelterBlocks;
  
  // Screen information, score and lives
  MSprite3D scoreText;
  MSprite3D livesText;
  MSprite3D[] livesMarkets;

  // Game status, current view, if paused 
  boolean view3d = true;
  boolean pause = true;

  // Game information  
  int score;
  int level;
  int lives;
  
  // Current invaders row 
  int rows;
  
  // Bounds of the invaders 
  int minCol;
  int maxCol;
  int minRow;
  int maxRow;  
  
  // Invaders counter by column and row
  int[] colCounter;
  int[] rowCounter;

  /**
   * Create a Space Invader Game
   */
  MSIGame()
  {
    // Create all the objects holders
    invaders = new MSprite3D[5][11];  
    playerBullets = new MMesh[10];
    invadersBullets = new MMesh[10];
    
    // Init the invaders counters
    colCounter = new int[11];
    rowCounter = new int[5];

    // Load all the images
    si1_1 = loadImage("si1_1.png");
    si1_2 = loadImage("si1_2.png");
    player_1 = loadImage("player.png");
    spaceship_1 = loadImage("spaceship.png");

    // Calculate the invader dimension
    siWidth = si1_1.width;
    siHeight = si1_1.height;

    // Get the current camera
    cam = m3d.activeCamera();
  }

  /**
   * Draw the game components and update positions and states
   */
  void draw()
  {
    // If the game is pause do nothing
    if(!pause)
    {
      // Don't move the invaders each time 
      // only each speed of frames
      if(frameCount % speed == 0)
        moveInvaders();

      // Move elements of the game
      movePlayer(playerDir);
      movePlayerBullets();
      moveInvaderBullets();
      moveSpaceship();
      
      // Check any bullet collision 
      checkPlayerBulletCollisions();
      checkInvadersBulletCollisions();
    }    
  }

  /**
   * Check user interaction
   */
  void keyPressed()
  {
    // Move player or fire bullets 
    switch(keyCode)
    {
    case LEFT :
      playerDir = LEFT; 
      break;
    case RIGHT : 
      playerDir = RIGHT;
      break;
    case FIRE :
      firePlayerBullet();
      break;
    }

    // Change the player view
    switch(key)
    {
    case '0' : 
      changeView(); 
      break;
    case '7' :
      cam.translate(0,5,0);
      break;
    case '9' :
      cam.translate(0,-5,0);
      break;
    }
  }

  /**
   * Stop moving the player
   */
  void keyReleased()
  {
    playerDir = -1;
  }

  /**
   * Start the game 
   */
  void startGame()
  {
    // Clear all the screen elements
    background(0);
    
    // Init the game state 
    lives = 3;
    maxInvadersBullets = 2;
    maxPlayerBullets = 2;
    
    // Create elements
    createPlayer();
    createInvaders();
    createShelters();
    
    // Update screen information
    updateScore();
    updateLives();
    
    // Calculate the first view 
    changeView();
    
    // Begin the action
    pause = false;
  }

  /**
   * Create the player object
   */
  void createPlayer()
  {
    // If no player, create one
    if(player == null)
    {
      // Create the player and add to the 3d world
      player = new MSprite3D(player_1);
      m3d.add(player);
    }
    
    // Move the player to the center and down of the screen
    player.location(0,0,7 * siWidth);
  }

  /**
   * Create the invaders objects
   */
  void createInvaders()
  {
    // Current inavder position
    int tmpX;
    int tmpZ = 0;

    // Create 5 rows of invaders
    for(int i=0; i<5; i++)
    {
      // Go to the beggining of the row
      tmpX = -(5 * (siWidth + siWidth/2));
     
      // Create 11 invaders per row
      for(int j=0; j<11; j++)
      {
        // Create the invader object
        invaders[i][j] = new MSprite3D(si1_1);
        invaders[i][j].location(tmpX,0,-tmpZ);
        m3d.add(invaders[i][j]);

        // Update the col position
        tmpX += siWidth + siWidth/2;
      }

      // Update the row position
      tmpZ += siHeight + (siHeight/2);
    }  
    
    // Init the row invaders counters
    for(int i=0; i<5; i++)
      rowCounter[i] = 11;
      
    // Init the col invaders counters
    for(int i=0; i<11; i++)
      colCounter[i] = 5;      
      
    // Init invaders bounds
    minCol = 0;
    maxCol = 10;
    minRow = 0;
    maxRow = 4;
    
    // Init number of invaders
    numInvaders = 5*11;
  }

  /**
   * Move the invaders one position
   */
  void moveInvaders()
  {
    // Init movements 
    int dx = -5;
    int dz =  0;

    // If the invaders march the number of steps 
    // move to front
    if(abs(countSteps) == steps)
    {
      // Change direction
      right = !right;
      
      // Init steps 
      countSteps = 0;
      
      // Move to from
      dz = 5;
      rows--;
      
      // Each five rows decrease the number of frames
      // to make a movement, don't decrease if maximum speed is reach
      if(rows % 5 == 0)
        if(speed != 1)
          speed--;

      // Check if the invaders touch grounds
      if(rows + (minRow * 10) == -45)
        gameover();
    }

    // Increase or decrease acoording direction
    if(right)
      dx *= -1;

    // Check if the first invader of the col already fire
    boolean fireBullet;

    // Move invaders
    for(int j=minCol; j<=maxCol; j++)
    {
      // Fire a bullet for each col
      fireBullet = true;
      
      // Move invader if exists 
      for(int i=minRow; i<=maxRow; i++)
        if(invaders[i][j] != null)
        {
          // Change the position
          invaders[i][j].translate(dx,0,dz);

          // If already fire 
          if(fireBullet)
          {
            // Try to fire 
            fireInvaderBullet(invaders[i][j]);
            
            // Don't allow more shots in the col
            fireBullet = false;
          }
          
/*         
        // Update the animation of the invader
        if(countSteps % 2 == 0)
            invaders[i][j].image(si1_1);
          else
           invaders[i][j].image(si1_2);          
*/
        }
    }

    // Add one step
    countSteps++;
  }

  /**
   * Fire a invader bullet 
   *
   * @param invader The invader to make the shot
   * @return true if the inaver shot, false otherwise
   */
  boolean fireInvaderBullet(MSprite3D invader)
  {
    // If the invaders already shot the max number of bullets 
    // do nothing
    if(numInvadersBullets >= maxInvadersBullets)
      return false;

    // Don't shot always, do it in a random way
    if(random(100) > 5)
      return false;

    // Create the bullet at the invader location
    fill(0xFFFF00);
    MMesh bullet = m3d.box(5,5,5);
    bullet.location(invader.x(),invader.y(),invader.z());

    // Try to put the bullet inside an empty space
    // in the current array of bullets
    for(int i=0; i<length(invadersBullets); i++)  
      if(invadersBullets[i] == null)
      {      
        // Update the bullet information
        invadersBullets[i] = bullet;
        numInvadersBullets++;
        return true;
      }  

    // Not bullet fire
    return false;
  }

  /**
   * Fire a player bullet 
   *
   * @return true if the inaver shot, false otherwise
   */
  boolean firePlayerBullet()
  {
    // If the player already fire the maximun number of bullets
    // Do nothing
    if(numPlayerBullets >= maxPlayerBullets)
      return false;

    // Create the bullet at the position of the player
    fill(0x00FF00);
    MMesh bullet = m3d.box(5,5,5);
    bullet.location(player.x(),player.y(),player.z());
    
    // Try to put the bullet inside an empty space
    // in the current array of bullets
    for(int i=0; i<length(playerBullets); i++)  
      if(playerBullets[i] == null)
      {
        // Update the bullet information
        playerBullets[i] = bullet;
        numPlayerBullets++;
        return true;
      }
      
    // Not bullet fire
    return false;
  }

  /**
   * Move the player bullets on the screen
   */
  void movePlayerBullets()
  {
    // Move each bullet of the player if exists
    for(int i=0; i<length(playerBullets); i++)  
      if(playerBullets[i] != null)
      {
        // Move bullet
        playerBullets[i].translate(0,0,-5);

        // Check if the bullet reach the end of the camp
        // and remove it
        if(playerBullets[i].z() < -8 * siHeight)
        {
          m3d.remove(playerBullets[i]);
          playerBullets[i] = null;
          numPlayerBullets--;
        }      
      }
  }

  /**
   * Move the player bullets on the screen
   */
  void moveInvaderBullets()
  {
    // Move each bullet of the player if exists
    for(int i=0; i<length(invadersBullets); i++)  
      if(invadersBullets[i] != null)
      {
        // Move Bullets
        invadersBullets[i].translate(0,0,+5);

        // Check if the bullet reach the end of the camp
        // and remove it
        if(invadersBullets[i].z() > player.z())
        {
          m3d.remove(invadersBullets[i]);
          invadersBullets[i] = null;
          numInvadersBullets--;
        }      
      }
  }

  /**
   * Check if any bullet of the player collides with some element
   */
  void checkPlayerBulletCollisions()
  {
    // Check each bullet of the player if exists    
    for(int k=0; k<length(playerBullets); k++)    
      if(playerBullets[k] != null)
      {
        // Check collision with alive invaders
        for(int i=minRow; i<=maxRow; i++)
          for(int j=minCol; j<=maxCol; j++)
            if(invaders[i][j] != null)
              // If the bullet hits a invader remove it 
              if(distance(invaders[i][j],playerBullets[k]) < 20)
              {
                // Decrease invaders counters
                colCounter[j]--;
                rowCounter[i]--;
                numInvaders--;              
                numPlayerBullets--;
                
                // Remove object of the scene
                m3d.remove(playerBullets[k]);
                m3d.remove(invaders[i][j]);                              
                invaders[i][j] = null;
                playerBullets[k] = null;  
                
                // Update score info
                score += (i+1) * 10;
                updateScore();
                         
                // Updates information of bounds
                updateColsAndRows();
                
                // Check if the destroy all invaders
                if(numInvaders == 0)
                  win();
                
                // Collision end
                return;
              }

        // Check if the bullet hits a shelter  
        for(int i=0; i<length(shelterBlocks); i++)
          if(shelterBlocks[i] != null)
            // if the bullet hits the shelter remove it  
            if(distance(shelterBlocks[i],playerBullets[k]) < 5)
            {
              // Remove objects
              m3d.remove(playerBullets[k]);
              m3d.remove(shelterBlocks[i]);
              shelterBlocks[i] = null;
              playerBullets[k] = null;
              
              // Decrease bullet counter
              numPlayerBullets--;
              return;
            }
            
        // Check if the bullet hits the spaceship
        if(spaceship != null)
          // If the bullets hits the spaceship
          if(distance(spaceship,playerBullets[k]) < 30)
          {
              // Remove Objects 
              m3d.remove(playerBullets[k]);
              m3d.remove(spaceship);
              playerBullets[k] = null;
              spaceship = null;
              
              // Update counter and score
              numPlayerBullets--;
              score += 100;
              updateScore();
              return;
          }
      }
  }

  /**
   * Check if any bullet of the player collides with some element
   */
  void checkInvadersBulletCollisions()
  {
    // Check each bullet of the player if exists 
    for(int i=0; i<length(invadersBullets); i++)
      if(invadersBullets[i] != null)
      {    
        // If the bullet hits the player
        if(distance(invadersBullets[i],player) < 30)
        {          
          // Remove the bullet
          m3d.remove(invadersBullets[i]);
          invadersBullets[i] = null;
          numInvadersBullets--;
          
          // The player was killed
          playerKilled();
          return;
        }

        // Check if the bullet hits a shelter  
        for(int j=0; j<length(shelterBlocks); j++)
          if(shelterBlocks[j] != null)         
            // if the bullet hits the shelter remove it            
            if(distance(shelterBlocks[j],invadersBullets[i]) < 5)
            {
              // Remove Objects
              m3d.remove(invadersBullets[i]);
              m3d.remove(shelterBlocks[j]);
              shelterBlocks[j] = null;
              invadersBullets[i] = null;
              numInvadersBullets--;
              return;
            }
      }
  }

  /**
   * Calculate the distance between two 3d objects
   *
   * @param obj1 object one 
   * @param obj2 object two
   * @return disnace between objects
   */
  int distance(M3dObject obj1, M3dObject obj2)
  {
    // Calculate diferences
    int tmpX = obj1.x() - obj2.x();
    int tmpY = obj1.y() - obj2.y();
    int tmpZ = obj1.z() - obj2.z();

    // Return distance
    return (int) Math.sqrt(tmpX*tmpX + tmpY*tmpY + tmpZ*tmpZ);
  }

  /**
   * Change the player view
   */
  void changeView()
  {
    // Update the view state 
    view3d = !view3d;

    // If 3D, put the camera in the player position
    if(view3d)
    {
      cam.location(player.x(),player.y(),player.z());
      cam.orientation(0,0,0,0);
    }
    // If 2D put the camera over the battle field
    else
    {
      cam.location(0,height*3,0);
      cam.orientation(90,-1,0,0);
    }
  }

  /**
   * Move the player 
   *
   * @param dir Player direction
   */
  void movePlayer(int dir)
  {
    // If dir not specified, do nothing
    if(dir == -1)
      return;

    // Calculate next position
    int dx = (dir == LEFT) ? -5 : 5;
    int tmpX = player.x() + dx;
    int bounds = (5 * (siWidth + siWidth/2));

    // If player outside field do nothing
    if(tmpX < -bounds || tmpX > bounds)
      return;

    // Move the player
    player.translate(dx,0,0);

    // If 3D, move the cam with the player
    if(view3d)
      cam.translate(dx,0,0);    
  }

  /**
   * Create the shelters
   */
  void createShelters()
  {
    // Init the shelters blocks
    shelterBlocks = new MMesh[80];

    // Calculate the first position
    int tmpX = (11 * (siWidth + siWidth/2));
    int tmpZ = 5 * siWidth;

    // Calculate the separation
    int dx = (tmpX - 70*4)/5;
    tmpX = -tmpX/2;

    // Create four shelters separate by dx
    createShelter(tmpX+1*dx,tmpZ,0);
    createShelter(tmpX+3*dx,tmpZ,20);
    createShelter(tmpX+5*dx,tmpZ,40);
    createShelter(tmpX+7*dx,tmpZ,60);
  }

  /**
   * Create a shelter in the x and z coordinates and storage the blocks 
   * after the index specified
   *
   * @param x x coordinate of the shelter
   * @param y y coordinate of the shelter
   * @param index the index to storage the blocks
   */   
  void createShelter(int x, int z, int index)
  {
    // Create green blocks
    fill(0x00FF00);

    // Current block positions and number
    int tmpX = x;
    int tmpZ = z;
    int blockCounter = 0;

    // For the shelter add blocks
    for(int i=0; i<4; i++)
    {
      for(int j=0; j<7; j++)
      {
        // If the shelter has a block add it
        if(shelter[i][j] == 1)
        {
          // Create the block 
          MMesh block = m3d.box(10,10,10);
          block.location(tmpX,0,tmpZ);        
          shelterBlocks[index + blockCounter] = block;
          blockCounter++;
        }

        // Update position 
        tmpX += 10;
      }   

      // Init x position again and update z 
      tmpX = x;
      tmpZ += 10;
    }  
  }

  /**
   * If player is killed, update and check game state
   */
  void playerKilled()
  {
    // Remove live and update info
    lives--;
    updateLives();
    
    // Alert the user
    phone.vibrate(333);
    phone.flash(333);
    
    // If any live, put the player again
    if(lives > 0)
      createPlayer();
    // Show the game over signal
    else
      gameover();
  }
  
  /**
   * Show the game over message
   */
  void gameover()
  {
    // Stop the game
    pause = true;
    
    // Put a big message on the screen
    fill(0x00FF00);
    m3d.text("GAME OVER",0,-100,0);
    M3dObject gameoverMessage = (M3dObject) m3d.lastObject();
    gameoverMessage.scale(5,5,5);
  }
  
  /**
   * Show the win message
   */
  void win()
  {
    // Stop the game
    pause = true;
    
    // Put a big win message
    fill(0x00FF00);
    m3d.text("WIN",0,-100,0);
    M3dObject winMessage = (M3dObject) m3d.lastObject();
    winMessage.scale(5,5,5);    
  }

  /**
   * Update the score at the screen
   */  
  void updateScore()
  {
    // Remove previous message
    if(scoreText != null)
      m3d.remove(scoreText);

    // Put a message in the corner with the score
    int tmpX = -(3 * (siWidth + siWidth/2));
    fill(0xFFFFFF);
    m3d.text("Score " + score,tmpX,-80,-siWidth*5-20);
    scoreText = (MSprite3D) m3d.lastObject();
    scoreText.scale(2,2,2);
  }  

  /**
   * Update lives information
   */
  void updateLives()
  {
    // If message not created yet, create it 
    if(livesText == null)
    {
      // Put text lives on the screen
      fill(0xFFFFFF);
      m3d.text("Lives ",0,-80,-siWidth*5-20);
      livesText = (MSprite3D) m3d.lastObject();
      livesText.scale(2,2,2);      
      
      // Create the lives markets
      livesMarkets = new MSprite3D[3];
      
      // Add each market next to the text
      for(int i=0; i<length(livesMarkets); i++)
      {
        livesMarkets[i] = (MSprite3D) m3d.duplicate(player);
        livesMarkets[i].location(70*(i+1),80,-siHeight*18);
        livesMarkets[i].scale(0.7,0.7,0.7);
        m3d.add(livesMarkets[i]);
      }
    }
    
    // Remove a live
    if(lives < 3 && livesMarkets[lives] != null)
      m3d.remove(livesMarkets[lives]);
  }  
  
  /**
   * Move the spaceship
   */
  void moveSpaceship()
  {
    // If no spaceship create, check if create one using random
    if(spaceship == null && random(1000) < 20)
    {
      // Create the spaceship with left or right direction
      spaceshipDir = random(100) > 50 ? -1 : 1;
      
      // Create thespaceship
      spaceship = new MSprite3D(spaceship_1);
      int tmpX = -(5 * (siWidth + siWidth/2));
      spaceship.location(tmpX*spaceshipDir,0,-siHeight*8);
      m3d.add(spaceship);
    }
    
    // If a spaceship is flying 
    if(spaceship != null)
    {
      // Update the position according the direction
      spaceship.translate(2*spaceshipDir,0,0);      
      
      // Check if the spaceship is out of the field
      // and remove it
      int tmpX = spaceship.x();      
      if(tmpX > (5 * (siWidth + siWidth/2)) || 
        tmpX < -(5 * (siWidth + siWidth/2)))
      {      
        m3d.remove(spaceship);
        spaceship = null;
      }
    }
  }

  /**
   * Update invaders bounds
   */  
  void updateColsAndRows()
  {
    // Update the row info
    minRow = Integer.MAX_VALUE;
    maxRow = Integer.MIN_VALUE;
    
    // Calculate the min and max row 
    for(int i=0; i<5; i++)
      if(rowCounter[i] != 0)
      {
       if(i < minRow)
        minRow = i;
       else
         maxRow = i;
      }

    // Update the cols info
    minCol = Integer.MAX_VALUE;
    maxCol = Integer.MIN_VALUE;
    
    // Calcuate yhe min and max cols
    for(int i=0; i<11; i++)
      if(colCounter[i] != 0)
      {
       if(i < minCol)
        minCol = i;
       else
         maxCol = i;
      }
  }
}
